#include "PlayManager.h"

#include "EventImplementation.h"
#include "PlayManagerImplementation.h"
#include "FMODSystem.h"

#include "api/inc/fmod.hpp"
#include "api/inc/fmod_errors.h"

#include <iostream>
#include <sstream>

namespace SnackBreak
{
namespace Impl
{
std::string FMODMixEvent::GenerateName(TriggerType t)
{
	// Could really just be a number except that FMOD syncpoints store char*...
	// .. so the type of mix followed by a number.

	static long whatMixWeUpTo = 0; // this'll be interesting for long sessions of use in the future (ha, better make it a long)
	
	const char mixTypeCodes[] = "DO"; // drop, overlap

	std::ostringstream newName;

	int chindex = t-FirstMixType;
	int len = strlen(mixTypeCodes);
	if (chindex < len)
	{
		newName.put(mixTypeCodes[chindex]);
		whatMixWeUpTo++;
		newName << whatMixWeUpTo;
	}

	return newName.str();
}

FMOD_RESULT F_CALLBACK SyncEvent::SyncCallback(
  FMOD_CHANNEL *  channel, 
  FMOD_CHANNEL_CALLBACKTYPE  type, 
  int  command, 
  unsigned int  commanddata1, 
  unsigned int  commanddata2
)
{
	MixCollection mixesToAction;
	FMOD::Channel* theChannel = reinterpret_cast<FMOD::Channel*>(channel);

	if ((type == FMOD_CHANNEL_CALLBACKTYPE_END) && (commanddata1 == 0) && (commanddata2 == 0)) 
	{
		// this is an 'end track' mix for the track on channel channel
		// search for mixevents whose:
		//  + mix.type == ENDOF
		//  + mix.outgoing.channel == channel
		// and SyncAction them.
		FMODPlayManager::Get().FindDropsOffChannel(theChannel, mixesToAction);
	}
	else if ((type == FMOD_CHANNEL_CALLBACKTYPE_SYNCPOINT)) 
	{
		// this is a 'sync point' mix for the track on channel channel
		// search for mixevents whose:
		//  + mix.type == syncpoint
		//  + mix.outgoing.channel == channel
		//  + mix.syncpointname = sound.getsyncpointname(sound.getsyncpoint(commanddata1))
		// and SyncAction them.
		FMOD::Sound* userData = NULL;
		theChannel->getUserData(reinterpret_cast<void**>(&userData));
		FMOD_SYNCPOINT* point;
		char name[256];
		if (userData != NULL)
		{
			userData->getSyncPoint(commanddata1, &point);		
			userData->getSyncPointInfo(point, name, sizeof(name), NULL, FMOD_TIMEUNIT_MS);
		}
		FMODPlayManager::Get().FindMixesOffChannel(theChannel, name, mixesToAction);
	}

	// now we have a list of things to do, walk it and make each happen
	for (MixCollection::iterator it = mixesToAction.begin(); it < mixesToAction.end(); it++)
		(*it)->SyncAction();

	return FMOD_OK;
}

void FMODMixEvent::PitchAndCueIncoming(double cueBeatNum/* = 0.0*/)
{
	double rateAdjust = outgoing->getCueInfo().pitchAdjustmentForOtherBeatLen(incoming->getCueInfo().myBeatLength());
	std::cout << "nudge factor for this mix: " << rateAdjust << std::endl;
	incoming->setPlayRate(rateAdjust);
	incoming->setPosition(cueBeatNum);
}

DropEvent::DropEvent(FMODTrack* o, FMODTrack* i)
:
	SyncEvent(o, i, SyncPoint)
{
	// set the callback to occur on end
	outgoing->getChannel()->setCallback(FMOD_CHANNEL_CALLBACKTYPE_END, SyncCallback, 0);
}

void DropEvent::SyncAction()
{
	// do not touch outgoing; it has already gone, it is not valid!
	// just kick off the next track
	incoming->Play();
}

OverlapMixEvent::OverlapMixEvent(FMODTrack* o, FMODTrack* i, double numBeats)
:
	SyncEvent(o, i, SyncPoint)
{
	eventLength = numBeats; 
	Setup();
}

void OverlapMixEvent::Setup()
{
	FMOD_RESULT result;

	eventStartBeat = (outgoing->getLastBeatNum() - eventLength);

	// determine msec stream position of mix (end - length) and..
	double msecSyncPos = outgoing->getCueInfo().myNthBeatIsAtMsec(eventStartBeat);

	// ..add a sync point there
	FMOD_SYNCPOINT* dontCare;
	result = outgoing->getSound()->addSyncPoint(msecSyncPos, FMOD_TIMEUNIT_MS, name.c_str(), &dontCare);
	ERRCHECK(result);

	// set the userData for the channel to be our Sound;
	// this allows us to hone our search to identify which Mix we need to SyncAction in the (onesizefitsall) callback
	result = outgoing->getChannel()->setUserData(outgoing->getSound()); // if we can get from channel->sound in callback, we can get from sound->syncpointinfo!
	ERRCHECK(result);

	// set the callback
	result = outgoing->getChannel()->setCallback(FMOD_CHANNEL_CALLBACKTYPE_SYNCPOINT, SyncCallback, 0);
	ERRCHECK(result);
};

double FMODMixEvent::CalculateFudgeAmount()
{
	double fudge = eventStartBeat - outgoing->getPosition();
	return fudge;
}

void OverlapMixEvent::SyncAction()
{
	std::cout << "Actioning " << getName() << std::endl;
	
	// Because the callback happens at a time _close_ to where we want,
	// we need to fudge the start time of incoming a lil bit.
	// (Hopefully an imperceptibly tiny bit!)
	double fudge = CalculateFudgeAmount();
	std::cout << "fudge factor for this mix: " << fudge << std::endl;

	// set incoming's pitch to appropriate, cue to beat zero(ish), and play it
	PitchAndCueIncoming(eventCueBeat - fudge);
	incoming->Play();
}

AutomationEvent::AutomationEvent(FMODTrack* o, FMODTrack* i)
:
	FMODMixEvent(o, i, DSP),
	stage(Waiting)
{

}

AutomationEvent::~AutomationEvent()
{
}

bool AutomationEvent::UpdateStage(double bufferDuration)
{
	bool changed = false;

	if (Outgoing() != NULL)
	{
		double outPos = Outgoing()->getPosition();
	
		// 
		double endOfBufferOutPos = outPos + (bufferDuration / Outgoing()->getCueInfo().myBeatLength());
		
		switch (stage)
		{
		case Waiting:
			if (endOfBufferOutPos >= getStartBeat())
			{
				stage = Happening;
				changed = true;
			}
		case Happening:
			if (endOfBufferOutPos >= getEndBeat())
			{
				stage = Finished;
				changed = true;
			}
		}
	}

	return changed;
}

BlendMixEvent::BlendMixEvent(FMODTrack* o, FMODTrack* i, double numBeats, double mixLen, double incoCueAt)
:
	AutomationEvent(o, i)
{
	// for now, all fades will be X beats long. 
	eventStartBeat = (outgoing->getLastBeatNum() - numBeats);
	eventLength = mixLen;
	eventCueBeat = incoCueAt;
}

void BlendMixEvent::AutomationAction(double bufferDuration)
{
	bool changed = UpdateStage(bufferDuration);

//	std::cerr << "Stage = " << stage << std::endl;

	// common logic alert!!
	// potential for some of this to be params to AutomationAction()
	if (stage == Happening)
	{ 
		// if we have only just started happening, we need to cue incoming
		if (changed) 
		{
			// we've got 2 kinds of fudge - prefudge (for overlaps outta sync callbacks)
			// and postfudge here, where we are going to be late no matter what
			double endOfBufferOutPos = Outgoing()->getPosition() + (bufferDuration / Outgoing()->getCueInfo().myBeatLength());
			double fudge = endOfBufferOutPos - eventStartBeat;
			PitchAndCueIncoming(eventCueBeat + fudge);
			Incoming()->Play();
		}

		double cur = Incoming()->getPosition();
		double mixLen = getEventLength();
		double mixProgress = (cur - eventCueBeat) / mixLen;
		float amp = 1.0 - mixProgress;

		if (amp < 0)
			amp = 0.0;
		if (amp > 1.0)
			amp = 1.0;
	
		Incoming()->setVolume(mixProgress);
		Outgoing()->setVolume(1.0 - mixProgress);
	}
	else if (changed && (stage == Finished))
		Outgoing()->Pause(); // harsh, see ya later thanks for coming
}

}; // namespace Impl
}; // namespace SnackBreak

